﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Globalization;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
using System.Web.UI;
using Lilium.Web.Mvc.MicrosoftAjax;

namespace Lilium.Web.Mvc.AjaxControlToolkit
{
	[ToolkitScriptReference("ExtenderBase.BaseScripts.js", LoadOrder = 0)]
	[ToolkitScriptReference("ExtenderBase.BaseScriptsExt.js", LoadOrder = 1)]
	public class ToolkitExtenderBase: MicrosoftAjaxBehavior, IUrlResolutionService
	{
		static ToolkitExtenderBase()
		{
			CustomConverters.Add(typeof(Color), delegate(object value) { return ColorTranslator.ToHtml((Color)value); });
			Converter<object, string> dateTimeConverter = delegate(object value)
			{
				DateTime? date = (DateTime?)value;
				return (date != null) ? date.Value.ToUniversalTime().ToString("r") : null;
			};
			CustomConverters.Add(typeof(DateTime), dateTimeConverter);
			CustomConverters.Add(typeof(DateTime?), dateTimeConverter);
		}

		public ToolkitExtenderBase(string scriptType, string elementID)
			: base(scriptType, elementID)
		{
		}

		/// <summary>
		/// Register script, load and render client state.
		/// </summary>
		/// <param name="ajaxHelper"></param>
		public override void RegisterScript(AjaxHelper ajaxHelper)
		{
			base.RegisterScript(ajaxHelper);
			if (EnableClientState)
			{
				LoadClientState(ajaxHelper);
				RenderClientState(ajaxHelper);
			}
		}

		public override void RenderScript(AjaxHelper ajaxHelper)
		{
			if (ajaxHelper == null)
				throw new ArgumentNullException("ajaxHelper");

			var scriptBehaviorDescriptor = new ScriptBehaviorBuilder(ScriptType, ElementID);
			AddScriptAttributes(scriptBehaviorDescriptor);

			var output = ajaxHelper.ViewContext.HttpContext.Response.Output;
			output.WriteLine("Sys.Application.add_init(function() {");
			output.WriteLine(scriptBehaviorDescriptor.GetScript());
			output.WriteLine("});");
		}

		#region public static IDictionary<Type, Converter<object, string>> CustomConverters

		private static readonly IDictionary<Type, Converter<object, string>> m_CustomConverters = new Dictionary<Type, Converter<object, string>>();

		/// <summary>
		/// Mapping of types to delegates that convert objects of that type to
		/// strings
		/// </summary>
		[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Type-aware public API")]
		public static IDictionary<Type, Converter<object, string>> CustomConverters { [DebuggerNonUserCodeAttribute] get { return m_CustomConverters; } }

		#endregion

		#region public string BehaviorID

		private string m_BehaviorID;

		[ExtenderControlProperty()]
		[ClientPropertyName("id")]
		[SuppressMessage("Microsoft.Naming", "CA1706:ShortAcronymsShouldBeUppercase", Justification = "Following ASP.NET AJAX pattern")]
		public string BehaviorID
		{
			[DebuggerNonUserCodeAttribute]
			get
			{
				return string.IsNullOrEmpty(m_BehaviorID) ? ElementID : m_BehaviorID;
			}
			[DebuggerNonUserCodeAttribute]
			set { m_BehaviorID = value; }
		}

		#endregion

		#region public string ClientState

		private string m_ClientState;

		[Browsable(false)]
		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public string ClientState { [DebuggerNonUserCodeAttribute] get { return m_ClientState; } [DebuggerNonUserCodeAttribute] set { m_ClientState = value; } }

		#endregion

		#region public string ClientStateFieldID

		private string m_ClientStateFieldID;

		[Browsable(false)]
		[EditorBrowsable(EditorBrowsableState.Never)]
		[ExtenderControlProperty()]
		//[IDReferenceProperty(typeof(HiddenField))]
		[DefaultValue("")]
		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		[SuppressMessage("Microsoft.Naming", "CA1706:ShortAcronymsShouldBeUppercase", Justification = "Following ASP.NET AJAX pattern")]
		public string ClientStateFieldID { [DebuggerNonUserCodeAttribute] get { return m_ClientStateFieldID ?? string.Empty; } [DebuggerNonUserCodeAttribute] set { m_ClientStateFieldID = value; } }

		#endregion

		#region public bool EnableClientState

		private bool m_EnableClientState;

		[Browsable(false)]
		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public bool EnableClientState { [DebuggerNonUserCodeAttribute] get { return m_EnableClientState; } [DebuggerNonUserCodeAttribute] set { m_EnableClientState = value; } }

		#endregion

		#region public EventHandler<EventArgs> ClientStateLoaded

		public EventHandler<EventArgs> ClientStateLoaded;

		protected virtual void OnClientStateLoaded(EventArgs args)
		{
			var handler = ClientStateLoaded;
			if (handler != null)
				handler(this, args);
		}

		#endregion

		#region IUrlResolutionService Members

		public string ResolveClientUrl(string relativeUrl)
		{
			return VirtualPathUtility.ToAbsolute(relativeUrl);
		}

		#endregion

		private void AddScriptAttributes(ScriptBehaviorDescriptor descriptor)
		{
			// validate preconditions
			if (descriptor == null)
				throw new ArgumentNullException("descriptor");

			// describe properties
			var properties = TypeDescriptor.GetProperties(this);
			foreach (PropertyDescriptor prop in properties)
			{
				ExtenderControlPropertyAttribute propAttr = null;
				ExtenderControlEventAttribute eventAttr = null;
				string propertyName = prop.Name;

				// Try getting a property attribute
				propAttr = (ExtenderControlPropertyAttribute)prop.Attributes[typeof(ExtenderControlPropertyAttribute)];
				if (propAttr == null || !propAttr.IsScriptProperty)
				{
					// Try getting an event attribute
					eventAttr = (ExtenderControlEventAttribute)prop.Attributes[typeof(ExtenderControlEventAttribute)];
					if (eventAttr == null || !eventAttr.IsScriptEvent)
					{
						continue;
					}
				}

				// attempt to rename the property/event
				ClientPropertyNameAttribute nameAttr = (ClientPropertyNameAttribute)prop.Attributes[typeof(ClientPropertyNameAttribute)];
				if (!string.IsNullOrEmpty(nameAttr.PropertyName))
				{
					propertyName = nameAttr.PropertyName;
				}

				// determine whether to serialize the value of a property.  readOnly properties should always be serialized
				bool serialize = prop.ShouldSerializeValue(this) || prop.IsReadOnly;
				if (serialize)
				{
					// get the value of the property, skip if it is null
					//Control c = null;
					object value = prop.GetValue(this);
					if (value == null)
					{
						continue;
					}

					// convert and resolve the value
					if (eventAttr != null && prop.PropertyType != typeof(String))
					{
						throw new InvalidOperationException("ExtenderControlEventAttribute can only be applied to a property with a PropertyType of System.String.");
					}
					else
					{
						if (!prop.PropertyType.IsPrimitive && !prop.PropertyType.IsEnum)
						{
							// Check if we can use any of our custom converters
							// (first do a direct lookup on the property type,
							// but also check all of its base types if nothing
							// was found)
							Converter<object, string> customConverter = null;
							if (!CustomConverters.TryGetValue(prop.PropertyType, out customConverter))
							{
								foreach (KeyValuePair<Type, Converter<object, string>> pair in CustomConverters)
								{
									if (prop.PropertyType.IsSubclassOf(pair.Key))
									{
										customConverter = pair.Value;
										break;
									}
								}
							}

							// Use the custom converter if found, otherwise use
							// its current type converter
							if (customConverter != null)
							{
								value = customConverter(value);
							}
							else
							{
								// Determine if we should let ASP.NET AJAX handle this type of conversion, as it supports JSON serialization
								if (propAttr != null && propAttr.UseJsonSerialization)
								{
									// Use ASP.NET JSON serialization
								}
								else
								{
									// Use the property's own converter
									TypeConverter conv = prop.Converter;
									value = conv.ConvertToString(null, CultureInfo.InvariantCulture, value);
								}
							}
						}
						/* Control references are not used in APS.MVC
						if (prop.Attributes[typeof(IDReferencePropertyAttribute)] != null && controlResolver != null)
						{
							c = controlResolver.ResolveControl((string)value);
						}
						*/
						if (prop.Attributes[typeof(UrlPropertyAttribute)] != null)
						{
							value = this.ResolveClientUrl((string)value);
						}
					}

					// add the value as an appropriate description
					if (eventAttr != null)
					{
						descriptor.AddEvent(propertyName, (string)value);
					}
					else if (prop.Attributes[typeof(ElementReferenceAttribute)] != null)
					{ // element reference contains client ID
						//if (c == null && controlResolver != null)
						//	c = controlResolver.ResolveControl((string)value);
						//if (c != null)
						//	value = c.ClientID;
						descriptor.AddElementProperty(propertyName, (string)value);
					}
					else if (prop.Attributes[typeof(ComponentReferenceAttribute)] != null)
					{ // same with component reference (probably subject of future changes)
						//if (c == null && controlResolver != null)
						//	c = controlResolver.ResolveControl((string)value);
						//if (c != null)
						//{
						//	ExtenderControlBase ex = c as ExtenderControlBase;
						//	if (ex != null && ex.BehaviorID.Length > 0)
						//		value = ex.BehaviorID;
						//	else
						//		value = c.ClientID;
						//}
						descriptor.AddComponentProperty(propertyName, (string)value);
					}
					else
					{
						//if (c != null)
						//	value = c.ClientID;
						descriptor.AddProperty(propertyName, value);
					}
				}
			}

			// determine if we should describe methods
			foreach (MethodInfo method in this.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public))
			{
				var methAttr = (ExtenderControlMethodAttribute)Attribute.GetCustomAttribute(method, typeof(ExtenderControlMethodAttribute));
				if (methAttr == null || !methAttr.IsScriptMethod)
				{
					continue;
				}
				/* methods are not currently supported
				// We only need to support emitting the callback target and registering the WebForms.js script if there is at least one valid method
				Control control = instance as Control;
				if (control != null)
				{
					// Force WebForms.js
					control.Page.ClientScript.GetCallbackEventReference(control, null, null, null);

					// Add the callback target
					descriptor.AddProperty("_callbackTarget", control.UniqueID);
				}
				*/
				break;
			}
		}

		/// <summary>
		/// This creates the field for any client state and sets up
		/// it's name.
		/// </summary>
		/// <returns></returns>
		private TagBuilder CreateClientStateField()
		{
			var hiddenField = new TagBuilder("input");
			hiddenField.MergeAttribute("type", "hidden");
			var clientStateFieldID = EnsureClientStateFieldID();
			hiddenField.MergeAttribute("id", clientStateFieldID);
			hiddenField.MergeAttribute("name", clientStateFieldID);
			return hiddenField;
		}

		/// <summary>
		/// Helper to generate a clientState ID
		/// </summary>
		/// <param name="index"></param>
		/// <returns></returns>
		private string EnsureClientStateFieldID()
		{
			var clientStateFieldID = ClientStateFieldID;
			if (string.IsNullOrEmpty(clientStateFieldID))
			{
				clientStateFieldID = string.Format(CultureInfo.InvariantCulture, "{0}_ClientState", BehaviorID);
				ClientStateFieldID = clientStateFieldID;
			}
			return clientStateFieldID;
		}

		/// <summary>
		/// Loads the values for each of the TargetProperties classes coming back from a postback.
		/// </summary>
		private void LoadClientState(AjaxHelper ajaxHelper)
		{
			if (ajaxHelper == null)
				throw new ArgumentNullException("ajaxHelper");

			var clientState = ajaxHelper.ViewContext.HttpContext.Request.Form[EnsureClientStateFieldID()];
			if (!string.IsNullOrEmpty(clientState))
			{
				ClientState = clientState;
				OnClientStateLoaded(EventArgs.Empty);
			}
		}

		/// <summary>
		/// Save any values in the TargetProperties objects out to client state so they are available
		/// on the client side.
		/// </summary>
		private void RenderClientState(AjaxHelper ajaxHelper)
		{
			if (ajaxHelper == null)
				throw new ArgumentNullException("ajaxHelper");

			var hiddenField = CreateClientStateField();
			var сlientState = ClientState;
			if (!string.IsNullOrEmpty(сlientState))
				hiddenField.MergeAttribute("value", сlientState);
			var output = ajaxHelper.ViewContext.HttpContext.Response.Output;
			output.WriteLine(hiddenField.ToString(TagRenderMode.SelfClosing));
		}

		#region private sealed class ScriptBehaviorBuilder

		private sealed class ScriptBehaviorBuilder: ScriptBehaviorDescriptor
		{
			public ScriptBehaviorBuilder(string type, string elementID)
				: base(type, elementID)
			{
			}

			public new string GetScript()
			{
				return base.GetScript();
			}
		}

		#endregion
	}
}